ποΈGitΠ―ΡΠ°ποΈ
docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md f07624be882e679affc02931dc75e8cb1594de18 (f07624be) Text, 10.29 KB
Tc9d1d9# Build-Logic Convention Patterns & Guidelines
Quick reference for maintaining and extending the build-logic convention system.
Tc9d1d9## Core Principles
Tff7b721. **DRY (Don't Repeat Yourself)**: Extract common configuration into functions
Tff7b722. **Clarity Over Cleverness**: Explicit intent in Ta5d6ff`build.gradle.kts` files matters
Tff7b723. **Single Responsibility**: Each convention plugin has one clear purpose
Tff7b724. **Test-Driven**: Configuration changes must pass Ta5d6ff`spotlessCheck`, Ta5d6ff`detekt`, and tests
Tc9d1d9## Convention Plugin Architecture
Ta5d6ff```
build-logic/
βββ convention/
β βββ src/main/kotlin/
β β βββ KmpFeatureConventionPlugin.kt # KMP feature modules (composes library + compose + koin + common deps)
β β βββ KmpLibraryConventionPlugin.kt # KMP modules: core libraries
β β βββ KmpLibraryComposeConventionPlugin.kt # KMP Compose Multiplatform setup
β β βββ KmpJvmAndroidConventionPlugin.kt # Opt-in jvmAndroidMain hierarchy for Android + desktop JVM
β β βββ AndroidApplicationConventionPlugin.kt # Main app
β β βββ AndroidLibraryConventionPlugin.kt # Android-only libraries
β β βββ AndroidApplicationComposeConventionPlugin.kt
β β βββ AndroidLibraryComposeConventionPlugin.kt
β β βββ org/meshtastic/buildlogic/
β β β βββ KotlinAndroid.kt # Base Kotlin/Android config
β β β βββ AndroidCompose.kt # Compose setup
β β β βββ FlavorResolution.kt # Flavor configuration
β β β βββ MeshtasticFlavor.kt # Flavor definitions
β β β βββ Detekt.kt # Static analysis
β β β βββ Spotless.kt # Code formatting
β β β βββ ... (other config modules)
```
Tc9d1d9## How to Add a New Convention
Tc9d1d9### Example: Adding a new test framework dependency
**Current Pattern (GOOD β
):**
If all KMP modules need a dependency, add it to Ta5d6ff`KotlinAndroid.kt::configureKmpTestDependencies()`:
Ta5d6ff```Ta5d6ffkotlin
Tff7b72internal Tff7b72fun Te6edf3ProjectTb4b4b4.Td2a8ffconfigureKmpTestDependenciesTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3KotlinMultiplatformExtensionTff7b72> Tb4b4b4{
Te6edf3sourceSetsTb4b4b4.Te6edf3apply Tb4b4b4{
Tff7b72val Te6edf3commonTest Tff7b72= Te6edf3findByNameTb4b4b4(Ta5d6ff"Ta5d6ffcommonTestTa5d6ff"Tb4b4b4) Tff7b72?: Tff7b72returnTf0883e@apply
Te6edf3commonTestTb4b4b4.Te6edf3dependencies Tb4b4b4{
Te6edf3implementationTb4b4b4(Te6edf3kotlinTb4b4b4(Ta5d6ff"Ta5d6fftestTa5d6ff"Tb4b4b4)Tb4b4b4)
T8b949e// NEW: Add here once, applies to all ~15 KMP modules
Te6edf3implementationTb4b4b4(Te6edf3libsTb4b4b4.Te6edf3libraryTb4b4b4(Ta5d6ff"Ta5d6ffnew-test-frameworkTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}
T8b949e// ... androidHostTest setup
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Ta5d6ff```
**Result:** All 15 feature and core modules automatically get the dependency β
Tc9d1d9### Example: Adding shared `jvmAndroidMain` code to a KMP module
**Current Pattern (GOOD β
):**
If a KMP module needs Java/JVM APIs shared between Android and desktop JVM, apply the opt-in convention plugin instead of manually creating source sets and Ta5d6ff`dependsOn(...)` edges:
Ta5d6ff```Ta5d6ffkotlin
Te6edf3plugins Tb4b4b4{
Te6edf3aliasTb4b4b4(Te6edf3libsTb4b4b4.Te6edf3pluginsTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3kmpTb4b4b4.Te6edf3libraryTb4b4b4)
Te6edf3idTb4b4b4(Ta5d6ff"Ta5d6ffmeshtastic.kmp.jvm.androidTa5d6ff"Tb4b4b4)
Tb4b4b4}
Te6edf3kotlin Tb4b4b4{
Te6edf3jvmTb4b4b4(Tb4b4b4)
Te6edf3android Tb4b4b4{ T8b949e/* ... */ Tb4b4b4}
Te6edf3sourceSets Tb4b4b4{
Te6edf3commonMainTb4b4b4.Te6edf3dependencies Tb4b4b4{ T8b949e/* ... */ Tb4b4b4}
Te6edf3jvmMainTb4b4b4.Te6edf3dependencies Tb4b4b4{ T8b949e/* jvm-only additions */ Tb4b4b4}
Te6edf3androidMainTb4b4b4.Te6edf3dependencies Tb4b4b4{ T8b949e/* android-only additions */ Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Ta5d6ff```
**Why:** The convention uses Kotlin's hierarchy template API to create Ta5d6ff`jvmAndroidMain` without the Ta5d6ff`Default Kotlin Hierarchy Template Not Applied Correctly` warning triggered by hand-written Ta5d6ff`dependsOn(...)` graphs.
Tc9d1d9### Example: Creating a new KMP feature module
**Current Pattern (GOOD β
):**
Use Ta5d6ff`meshtastic.kmp.feature` for any Ta5d6ff`feature:*` module. It composes Ta5d6ff`kmp.library` + Ta5d6ff`kmp.library.compose` + Ta5d6ff`koin` and provides all the common Compose/Lifecycle/Koin/Android dependencies that every feature needs:
Ta5d6ff```Ta5d6ffkotlin
Te6edf3plugins Tb4b4b4{
Te6edf3aliasTb4b4b4(Te6edf3libsTb4b4b4.Te6edf3pluginsTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3kmpTb4b4b4.Te6edf3featureTb4b4b4)
T8b949e// Optional: add only if this feature needs serialization
Te6edf3aliasTb4b4b4(Te6edf3libsTb4b4b4.Te6edf3pluginsTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3kotlinxTb4b4b4.Te6edf3serializationTb4b4b4)
Tb4b4b4}
Te6edf3kotlin Tb4b4b4{
Te6edf3jvmTb4b4b4(Tb4b4b4)
Te6edf3android Tb4b4b4{
Te6edf3namespace Tff7b72= Ta5d6ff"Ta5d6fforg.meshtastic.feature.yourfeatureTa5d6ff"
Te6edf3androidResourcesTb4b4b4.Te6edf3enable Tff7b72= Tff7b72false
Te6edf3withHostTest Tb4b4b4{ Te6edf3isIncludeAndroidResources Tff7b72= Tff7b72true Tb4b4b4}
Tb4b4b4}
Te6edf3sourceSets Tb4b4b4{
Te6edf3commonMainTb4b4b4.Te6edf3dependencies Tb4b4b4{
T8b949e// Only module-SPECIFIC deps here
Te6edf3implementationTb4b4b4(Te6edf3projectsTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3commonTb4b4b4)
Te6edf3implementationTb4b4b4(Te6edf3projectsTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3modelTb4b4b4)
Te6edf3implementationTb4b4b4(Te6edf3projectsTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3uiTb4b4b4)
Tb4b4b4}
Te6edf3androidMainTb4b4b4.Te6edf3dependencies Tb4b4b4{
T8b949e// Only Android-specific extras here
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Ta5d6ff```
**What the plugin provides automatically:**
Tff7b72- Ta5d6ff`commonMain`: Ta5d6ff`compose-multiplatform-material3`, Ta5d6ff`compose-multiplatform-materialIconsExtended`, Ta5d6ff`jetbrains-lifecycle-viewmodel-compose`, Ta5d6ff`koin-compose-viewmodel`, Ta5d6ff`kermit`
Tff7b72- Ta5d6ff`androidMain`: Ta5d6ff`androidx-compose-bom` (platform), Ta5d6ff`accompanist-permissions`, Ta5d6ff`androidx-activity-compose`, Ta5d6ff`androidx-compose-material3`, Ta5d6ff`androidx-compose-material-iconsExtended`, Ta5d6ff`androidx-compose-ui-text`, Ta5d6ff`androidx-compose-ui-tooling-preview`
Tff7b72- Ta5d6ff`commonTest`: Ta5d6ff`core:testing`
**Why:** Eliminates ~15 duplicate dependency declarations per feature module (modelled after Now in Android's Ta5d6ff`AndroidFeatureImplConventionPlugin`).
Tc9d1d9### Example: Adding Android-specific test config
**Pattern:** Add to Ta5d6ff`AndroidLibraryConventionPlugin.kt`:
Ta5d6ff```Ta5d6ffkotlin
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3LibraryExtensionTff7b72> Tb4b4b4{
Te6edf3configureKotlinAndroidTb4b4b4(Tff7b72thisTb4b4b4)
Te6edf3testOptionsTb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3animationsDisabled Tff7b72= Tff7b72true
T8b949e// NEW: Android-specific test config
Te6edf3unitTestsTb4b4b4.Te6edf3isIncludeAndroidResources Tff7b72= Tff7b72true
Tb4b4b4}
Tb4b4b4}
Ta5d6ff```
**Alternative:** If it applies to both app and library, consider extracting a function:
Ta5d6ff```Ta5d6ffkotlin
Tff7b72internal Tff7b72fun Te6edf3ProjectTb4b4b4.Td2a8ffconfigureAndroidTestOptionsTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3CommonExtensionTff7b72> Tb4b4b4{
Te6edf3testOptionsTb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3animationsDisabled Tff7b72= Tff7b72true
T8b949e// Shared test options
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Ta5d6ff```
Tc9d1d9## Duplication Heuristics
**When to consolidate (DRY):**
Tff7b72- β
Configuration appears in 3+ convention plugins
Tff7b72- β
The duplication changes together (same reasons to update)
Tff7b72- β
Extraction doesn't require complex type gymnastics
Tff7b72- β
Underlying Gradle extension is the same (Ta5d6ff`CommonExtension`)
**When to keep separate (Clarity):**
Tff7b72- β
Different Gradle extension types (Ta5d6ff`ApplicationExtension` vs Ta5d6ff`LibraryExtension`)
Tff7b72- β
Plugin intent is explicit in Ta5d6ff`build.gradle.kts` usage
Tff7b72- β
Duplication is small (<50 lines) and stable
Tff7b72- β
Future divergence between app/library handling is plausible
**Examples in codebase:**
| Duplication | Status | Reasoning |
|-------------|--------|-----------|
| Ta5d6ff`AndroidApplicationComposeConventionPlugin` β Ta5d6ff`AndroidLibraryComposeConventionPlugin` | **Kept Separate** | Different extension types; small duplication; explicit intent |
| Ta5d6ff`AndroidApplicationFlavorsConventionPlugin` β Ta5d6ff`AndroidLibraryFlavorsConventionPlugin` | **Kept Separate** | Different extension types; small duplication; explicit intent |
| Ta5d6ff`configureKmpTestDependencies()` (7 modules) | **Consolidated** | Large duplication; single source of truth; all KMP modules benefit |
| Ta5d6ff`jvmAndroidMain` hierarchy setup (4 modules) | **Consolidated** | Shared KMP hierarchy pattern; avoids manual Ta5d6ff`dependsOn(...)` edges and hierarchy warnings |
Tc9d1d9## Testing Convention Changes
After modifying a convention plugin, verify:
Ta5d6ff```Ta5d6ffbash
T8b949e# 1. Code quality
./gradlew spotlessCheck detekt
T8b949e# 2. Compilation
./gradlew assembleDebug assembleRelease
T8b949e# 3. Tests
./gradlew Tffa657test T8b949e# All unit tests
./gradlew :feature:messaging:jvmTest T8b949e# Feature module tests
./gradlew :feature:node:testAndroidHostTest T8b949e# Android host tests
Ta5d6ff```
Tc9d1d9## Documentation Requirements
When you add/modify a convention:
Tff7b721. **Add Kotlin docs** to the function:
Ta5d6ff ```Ta5d6ffkotlin
T8b949e/**
* Configure test dependencies for KMP modules.
*
* Automatically applies kotlin("test") to:
* - commonTest source set (all targets)
* - androidHostTest source set (Android-only)
*
* Usage: Called automatically by KmpLibraryConventionPlugin
*/
Tff7b72internal Tff7b72fun Te6edf3ProjectTb4b4b4.Td2a8ffconfigureKmpTestDependenciesTb4b4b4(Tb4b4b4) Tb4b4b4{ Tb4b4b4.Tb4b4b4.Tb4b4b4. Tb4b4b4}
Ta5d6ff ```
Tff7b722. **Update AGENTS.md** if convention affects developers
Tff7b723. **Update this guide** if pattern changes
Tc9d1d9## Performance Tips
Tff7b72- **Configuration-time:** Convention logic runs during Gradle configuration (0.5-2s)
Tff7b72- **Build-time:** No impact (conventions don't execute tasks)
Tff7b72- **Optimization focus:** Minimize Ta5d6ff`extensions.configure()` blocks (lazy evaluation is preferred)
Tc9d1d9### Good β
Ta5d6ff```Ta5d6ffkotlin
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3KotlinMultiplatformExtensionTff7b72> Tb4b4b4{
T8b949e// Single block for all source set configuration
Te6edf3sourceSetsTb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3commonTestTb4b4b4.Te6edf3dependencies Tb4b4b4{ T8b949e/* ... */ Tb4b4b4}
Te6edf3androidHostTestTff7b72?.Te6edf3dependencies Tb4b4b4{ T8b949e/* ... */ Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Ta5d6ff```
Tc9d1d9### Avoid β
Ta5d6ff```Ta5d6ffkotlin
T8b949e// Multiple blocks - slower configuration
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3KotlinMultiplatformExtensionTff7b72> Tb4b4b4{
Te6edf3sourceSetsTb4b4b4.Te6edf3getByNameTb4b4b4(Ta5d6ff"Ta5d6ffcommonTestTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3dependencies Tb4b4b4{ T8b949e/* ... */ Tb4b4b4}
Tb4b4b4}
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3KotlinMultiplatformExtensionTff7b72> Tb4b4b4{
Te6edf3sourceSetsTb4b4b4.Te6edf3getByNameTb4b4b4(Ta5d6ff"Ta5d6ffandroidHostTestTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3dependencies Tb4b4b4{ T8b949e/* ... */ Tb4b4b4}
Tb4b4b4}
Ta5d6ff```
Tc9d1d9## Common Pitfalls
Tc9d1d9### β **Mistake: Adding dependencies in the wrong place**
Ta5d6ff```Ta5d6ffkotlin
T8b949e// WRONG: Adds to ALL modules, not just KMP
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3ProjectTff7b72> Tb4b4b4{
Te6edf3dependencies Tb4b4b4{ Te6edf3addTb4b4b4(Ta5d6ff"Ta5d6ffimplementationTa5d6ff"Tb4b4b4, Tb4b4b4.Tb4b4b4.Tb4b4b4.Tb4b4b4) Tb4b4b4} T8b949e// Global!
Tb4b4b4}
T8b949e// RIGHT: Scoped to specific source set/module type
Te6edf3commonTestTb4b4b4.Te6edf3dependencies Tb4b4b4{ Te6edf3implementationTb4b4b4(Tb4b4b4.Tb4b4b4.Tb4b4b4.Tb4b4b4) Tb4b4b4}
Ta5d6ff```
Tc9d1d9### β **Mistake: Extension type mismatch**
Ta5d6ff```Ta5d6ffkotlin
T8b949e// WRONG: LibraryExtension isn't a subtype of ApplicationExtension
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3ApplicationExtensionTff7b72> Tb4b4b4{
T8b949e// Won't apply to library modules
Tb4b4b4}
T8b949e// RIGHT: Use CommonExtension or specific types
Te6edf3extensionsTb4b4b4.Te6edf3configureTff7b72<Te6edf3CommonExtensionTff7b72> Tb4b4b4{
T8b949e// Applies to both
Tb4b4b4}
Ta5d6ff```
Tc9d1d9### β **Mistake: Side effects during configuration**
Ta5d6ff```Ta5d6ffkotlin
T8b949e// WRONG: Eager task configuration at plugin-apply time
Te6edf3tasksTb4b4b4.Te6edf3withTypeTff7b72<Te6edf3TestTff7b72> Tb4b4b4{
T8b949e// Can realize tasks too early
Tb4b4b4}
T8b949e// RIGHT: Lazy, configuration-cache-friendly wiring
Te6edf3tasksTb4b4b4.Te6edf3withTypeTff7b72<Te6edf3TestTff7b72>Tb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3configureEach Tb4b4b4{
T8b949e// Applies to existing and future tasks lazily
Tb4b4b4}
Ta5d6ff```
Tc9d1d9## Related Files
Tff7b72- Ta5d6ff`AGENTS.md` - Development guidelines (Section 3.B testing, Section 4.A build protocol)
Tff7b72- Ta5d6ff`build-logic/convention/build.gradle.kts` - Convention plugin build config
Served by rngit 1.5.2 - Generated in 0.13s